package com.nulldev.util.java;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import com.nulldev.util.io.FileUtil;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.other.ArrayUtils;

public class ReflectionUtils {

	private static final Logger log = LoggerFactory.getLogger();
	
	public static class ClassUtil {
		/**
		 * Private helper method
		 * 
		 * @param directory
		 *            The directory to start with
		 * @param pckgname
		 *            The package name to search for. Will be needed for getting the
		 *            Class object.
		 * @param classes
		 *            if a file isn't loaded but still is in the directory
		 * @throws ClassNotFoundException
		 */
		private static void checkDirectory(File directory, String pckgname,
		        ArrayList<Class> classes) throws ClassNotFoundException {
		    File tmpDirectory;

		    if (directory.exists() && directory.isDirectory()) {
		        final String[] files = directory.list();

		        for (final String file : files) {
		            if (file.endsWith(".class")) {
		                try {
		                    classes.add(Class.forName(pckgname + '.'
		                            + file.substring(0, file.length() - 6)));
		                } catch (final NoClassDefFoundError e) {
		                    // do nothing. this class hasn't been found by the
		                    // loader, and we don't care.
		                }
		            } else if ((tmpDirectory = new File(directory, file))
		                    .isDirectory()) {
		                checkDirectory(tmpDirectory, pckgname + "." + file, classes);
		            }
		        }
		    }
		}

		/**
		 * Private helper method.
		 * 
		 * @param connection
		 *            the connection to the jar
		 * @param pckgname
		 *            the package name to search for
		 * @param classes
		 *            the current ArrayList of all classes. This method will simply
		 *            add new classes.
		 * @throws ClassNotFoundException
		 *             if a file isn't loaded but still is in the jar file
		 * @throws IOException
		 *             if it can't correctly read from the jar file.
		 */
		private static void checkJarFile(JarURLConnection connection,
		        String pckgname, ArrayList<Class> classes)
		        throws ClassNotFoundException, IOException {
		    final JarFile jarFile = connection.getJarFile();
		    final Enumeration<JarEntry> entries = jarFile.entries();
		    String name;

		    for (JarEntry jarEntry = null; entries.hasMoreElements()
		            && ((jarEntry = entries.nextElement()) != null);) {
		        name = jarEntry.getName();

		        if (name.contains(".class")) {
		            name = name.substring(0, name.length() - 6).replace('/', '.');

		            if (name.contains(pckgname)) {
		                classes.add(Class.forName(name));
		            }
		        }
		    }
		}

		/**
		 * Attempts to list all the classes in the specified package as determined
		 * by the context class loader
		 * 
		 * @param pckgname
		 *            the package name to search
		 * @return a list of classes that exist within that package
		 * @throws ClassNotFoundException
		 *             if something went wrong
		 */
		public static ArrayList<Class> getClassesForPackage(String pckgname)
		        throws ClassNotFoundException {
		    final ArrayList<Class> classes = new ArrayList<Class>();

		    try {
		        final ClassLoader cld = Thread.currentThread()
		                .getContextClassLoader();

		        if (cld == null)
		            throw new ClassNotFoundException("Can't get class loader.");

		        final Enumeration<URL> resources = cld.getResources(pckgname
		                .replace('.', '/'));
		        URLConnection connection;

		        for (URL url = null; resources.hasMoreElements()
		                && ((url = resources.nextElement()) != null);) {
		            try {
		                connection = url.openConnection();

		                if (connection instanceof JarURLConnection) {
		                    checkJarFile((JarURLConnection) connection, pckgname,
		                            classes);
		                } else if (connection.getClass().getCanonicalName().equals( "sun.net.www.protocol.file.FileURLConnection")) { //(connection instanceof FileURLConnection) {
		                    try {
		                        checkDirectory(
		                                new File(URLDecoder.decode(url.getPath(),
		                                        "UTF-8")), pckgname, classes);
		                    } catch (final UnsupportedEncodingException ex) {
		                        throw new ClassNotFoundException(
		                                pckgname
		                                        + " does not appear to be a valid package (Unsupported encoding)",
		                                ex);
		                    }
		                } else
		                    throw new ClassNotFoundException(pckgname + " ("
		                            + url.getPath()
		                            + ") does not appear to be a valid package");
		            } catch (final IOException ioex) {
		                throw new ClassNotFoundException(
		                        "IOException was thrown when trying to get all resources for "
		                                + pckgname, ioex);
		            }
		        }
		    } catch (final NullPointerException ex) {
		        throw new ClassNotFoundException(
		                pckgname
		                        + " does not appear to be a valid package (Null pointer exception)",
		                ex);
		    } catch (final IOException ioex) {
		        throw new ClassNotFoundException(
		                "IOException was thrown when trying to get all resources for "
		                        + pckgname, ioex);
		    }

		    return classes;
		}

		public static ArrayList<Class> getClasses(final String... packages) throws ClassNotFoundException {
			final ArrayList<Class> cls = new ArrayList<Class>();
			for (final String s : packages) {
				cls.addAll(getClassesForPackage(s));
			}
			return cls;
		}
		
		public static HashMap<String, File> queryItemsFolder(final String path) throws IOException {
			final HashMap<String, File> out = new HashMap<String, File>();
			final File jarFile = new File(JVMUtil.class.getProtectionDomain().getCodeSource().getLocation().getPath());

			if(jarFile.isFile()) {
			    final JarFile jar = new JarFile(jarFile);
			    final Enumeration<JarEntry> entries = jar.entries();
			    while(entries.hasMoreElements()) {
			    	final JarEntry ent = entries.nextElement();
			        final String name = ent.getName();
			        if (name.startsWith(path + "/") && name.contains(".")) {
			           //out.add(extract(name));
			        	//System.out.println("Found >> " + "/" + FileUtil.getFileName(name));
			        	out.put(FileUtil.getFileName(name), FileUtil.getResourceAsFile(ReflectionUtils.class, "/" + name));
			        }
			    }
			    jar.close();
			} else {
			   //final URL url = JVMUtil.class.getResource("/" + path);
				final File url = getResourceAsFile("/" + path);
			    if (url != null) {
			    	//System.out.println("/" + path);
			        try {
			            final File apps = url;
			            for (File app : FileUtil.listFilesForFolder(apps)) {//apps.listFiles()) {
			                out.put(app.getName(), app);
			            }
			        } catch (Exception ex) {
			        	ex.printStackTrace();
			        }
			    }
			}
			
			return out;
		}
	}
	
	public static class JarUtils {
		private static final Class<?>[] parameters = new Class[] {
				URL.class
		};

	    /**
	     * Adds a file to the classpath.
	     * @param s a String pointing to the file
	     * @throws IOException
	     */
	    public static void addFile(String s) throws IOException {
	        File f = new File(s);
	        addFile(f);
	    }

	    /**
	     * Adds a file to the classpath
	     * @param f the file to be added
	     * @throws IOException
	     */
	    public static void addFile(File f) throws IOException {
	        addURL(f.toURI().toURL());
	    }

	    /**
	     * Adds the content pointed by the URL to the classpath.
	     * @param u the URL pointing to the content to be added
	     * @throws IOException
	     */
	    public static void addURL(URL u) throws IOException {
	        URLClassLoader sysloader = (URLClassLoader) ClassLoader.getSystemClassLoader();
	        Class<?> sysclass = URLClassLoader.class;
	        try {
	            Method method = sysclass.getDeclaredMethod("addURL", parameters);
	            method.setAccessible(true);
	            method.invoke(sysloader, new Object[]{u}); 
	        } catch (Throwable t) {
	            t.printStackTrace();
	            throw new IOException("Error, could not add URL to system classloader");
	        }        
	    }
	}
	
	public static void listFields(final Object targetObject) {
		try {
			final Field[] fields = ArrayUtils.addAll(targetObject.getClass().getFields(), targetObject.getClass().getDeclaredFields());
			for (final Field f : fields) {
				System.out.println("Field '" + f.getName() + "' has a value of '" + f.get(new Object()) + "'.");
			}
		}catch (Exception e) {
			throw new RuntimeException("Failed to fetch fields.");
		}
	}
	
	public static void listMethods(final Object targetObject) {
		try {
			final Method[] fields = ArrayUtils.addAll(targetObject.getClass().getMethods(),
					targetObject.getClass().getDeclaredMethods());
			for (final Method f : fields) {
				System.out.println("Method '" + f.getName() + "'.");
			}
		}catch (Exception e) {
			throw new RuntimeException("Failed to fetch fields.");
		}
	}
	
	/**
	 * Set a field value
	 * @param targetObject [class] (Cannot be Foo.class must be a instance from new Foo() or a field with new Foo())
	 * @param fieldName Name of field such as itemVal
	 * @param fieldValue New value to set
	 * @return
	 */
	public static void setField(final Object targetObject, final String fieldName, final Object fieldValue) {
	    Field field;
	    try {
	        field = targetObject.getClass().getDeclaredField(fieldName);
	    } catch (NoSuchFieldException e) {
	        throw new RuntimeException("Failed to find field in class.");
	    }
	    Class superClass = targetObject.getClass().getSuperclass();
	    while (field == null && superClass != null) {
	        try {
	            field = superClass.getDeclaredField(fieldName);
	        } catch (NoSuchFieldException e) {
	            superClass = superClass.getSuperclass();
	        }
	    }
	    field.setAccessible(true);
	    try {
	        field.set(targetObject, fieldValue);
	    } catch (IllegalAccessException e) {
	    	throw new RuntimeException("Failed to modify field.");
	    }
	}

	/**
	 * Get a field value
	 * @param targetObject [class] (Cannot be Foo.class must be a instance from new Foo() or a field with new Foo())
	 * @param fieldName Name of field such as itemVal
	 * @return
	 */
	public static Object fetchField(final Object targetObject, final String fieldName) {
		Field field;
	    try {
	        field = targetObject.getClass().getDeclaredField(fieldName);
	    } catch (NoSuchFieldException e) {
	        throw new RuntimeException("Failed to find field in class.");
	    }
	    Class superClass = targetObject.getClass().getSuperclass();
	    while (field == null && superClass != null) {
	        try {
	            field = superClass.getDeclaredField(fieldName);
	        } catch (NoSuchFieldException e) {
	            superClass = superClass.getSuperclass();
	        }
	    }
	    field.setAccessible(true);
	    Object out;
	    try {
	    	out = field.get(targetObject);
	    }catch (Exception e) {
	    	throw new RuntimeException("Failed to fetch field.");
	    }
	    return out;
	}

	/**
	 * Call a function
	 * @param targetClass (Cannot be Foo.class must be a instance from new Foo() or a field with new Foo())
	 * @param fieldName (Name of the function)
	 * @param inputs (Variables in foo.function() - If there are none, leave it as null.)
	 * @return
	 */
	public static Object invokeFunction(final Object targetClass, final String fieldName, final Object... inputs) {
		Method method;
		Object output = new Object();
		try {
			method = targetClass.getClass().getDeclaredMethod(fieldName, ArrayUtils.classes(inputs));
		}catch (Exception e) {
			e.printStackTrace();
			try {
				method = targetClass.getClass().getMethod(fieldName, null);
			}catch (Exception e2) {
				if (log.debugEnabled) {
					log.debug("Printing functions...");
					for (final Method t : targetClass.getClass().getMethods()) {
						log.debug("\t" + t.getName() + " - func() by " + t.getDeclaringClass().getName());
					}
				}
				throw new RuntimeException("Failed to find method '" + fieldName + "'.");
			}
		}
		try {
			method.invoke(output, inputs);
		}catch (Exception e) {
			try {
				method.setAccessible(true);
				method.invoke(output, inputs);
			}catch (Exception e2) {
				e2.printStackTrace();
				throw new RuntimeException("Failed to invoke method.");
			}
		}
		return output;
	}
	
	public static <T> T invokeFunctionT(final Object targetClass, final String fieldName, final Object... inputs) {
		Method method;
		Object output = new Object();
		try {
			method = targetClass.getClass().getDeclaredMethod(fieldName, null);
		}catch (Exception e) {
			try {
				method = targetClass.getClass().getMethod(fieldName, null);
			}catch (Exception e2) {
				if (log.debugEnabled) {
					log.debug("Printing functions...");
					for (final Method t : targetClass.getClass().getMethods()) {
						log.debug("\t" + t.getName() + " - func() by " + t.getDeclaringClass().getName());
					}
				}
				throw new RuntimeException("Failed to find method '" + fieldName + "'.");
			}
		}
		try {
			method.<T>invoke(output, inputs);
		}catch (Exception e) {
			try {
				method.setAccessible(true);
				method.<T>invoke(output, inputs);
			}catch (Exception e2) {
				e2.printStackTrace();
				throw new RuntimeException("Failed to invoke method.");
			}
		}
		return (T) output;
	}

	public static Object verifyResult(final Class<?> expectedClass, final Object input) {
		if (expectedClass.isAssignableFrom(input.getClass())) {
			return input;
		}else {
			throw new RuntimeException("verifyResult failed. [Object Class mismatch. expectedClass.class={" + expectedClass.getName() + "}, object.class={" + input.getClass().getName() + "}]");
		}
	}

	public static boolean classExists(final String fullClassPath) {
		try {
			Class.forName(fullClassPath);
			return true;
		}catch (Exception e) {
			return false;
		}
	}

	public static Class<?> fetchClass(final String fullClassPath) {
		try {
			return Class.forName(fullClassPath);
		}catch (Exception e) {
			throw new RuntimeException("Class not found.");
		}
	}

	public static <T> Class<T> fetchClassT(final String fullClassPath) {
		try {
			return (Class<T>) Class.forName(fullClassPath);
		}catch (Exception e) {
			throw new RuntimeException("Class not found.");
		}
	}

	public static Object createInstance(final String className) {
		try {
			final Constructor cns = Class.forName(className).getConstructor();
			cns.setAccessible(true);
			return cns.newInstance();
		} catch (Exception e) {
			try {
				final Constructor cns = Class.forName(className).getDeclaredConstructor();
				cns.setAccessible(true);
				return cns.newInstance();
			}catch (Exception e3) {
				try {
					return Class.forName(className).newInstance();
				}catch (Exception e2) {
					try {
						return ReflectionUtils.class.getClassLoader().loadClass(className).newInstance();
					}catch (Exception ef) {
						log.error("Failed to create instance.", ef);
						return new Object();
					}
				}
			}
		}
	}

	public static Object createInstance(final String className, final Object... vars) {
		try {
			return Class.forName(className).getConstructor().newInstance(vars);
		} catch (final Exception e) {
			try {
				return Class.forName(className).newInstance();
			}catch (final Exception e1) {
				throw new RuntimeException("Failed to create new instance. [className=" + className + "]", e);
			}
		}
	}

	public static Object createInstance(final Class<?> cls) {
		return createInstance(cls.getName());
	}

	public static Object createInstance(final Class<?> cls, final Object... vars) {
		try {
			return cls.getConstructor().newInstance(vars);
		} catch (Exception e) {
			throw new RuntimeException("Failed to create new instance.");
		}
	}

    public static boolean isPrimitiveOrWrapper(final Class<?> type) {
        return type != null && (type.isPrimitive() || isPrimitiveWrapper(type));
    }

    public static boolean isPrimitiveWrapper(final Class<?> type) {
        return wrapperPrimitiveMap.containsKey(type);
    }

    private static final Map<Class<?>, Class<?>> wrapperPrimitiveMap = new HashMap<Class<?>, Class<?>>();

    static {
    	if (wrapperPrimitiveMap.isEmpty()) {
    		final Map<Class<?>, Class<?>> primitiveWrapperMap = new HashMap<Class<?>, Class<?>>();
    		primitiveWrapperMap.put(Boolean.TYPE, Boolean.class);
            primitiveWrapperMap.put(Byte.TYPE, Byte.class);
            primitiveWrapperMap.put(Character.TYPE, Character.class);
            primitiveWrapperMap.put(Short.TYPE, Short.class);
            primitiveWrapperMap.put(Integer.TYPE, Integer.class);
            primitiveWrapperMap.put(Long.TYPE, Long.class);
            primitiveWrapperMap.put(Double.TYPE, Double.class);
            primitiveWrapperMap.put(Float.TYPE, Float.class);
            primitiveWrapperMap.put(Void.TYPE, Void.TYPE);
            for (final Map.Entry<Class<?>, Class<?>> entry : primitiveWrapperMap.entrySet()) {
                final Class<?> primitiveClass = entry.getKey();
                final Class<?> wrapperClass = entry.getValue();
                if (!primitiveClass.equals(wrapperClass)) {
                    wrapperPrimitiveMap.put(wrapperClass, primitiveClass);
                }
            }
    	}
    }

	public static Method getMethod(final Class<?> cls, final String method) throws NoSuchMethodException, SecurityException {
		return cls.getMethod(method, null);
	}
	
	public static File getResourceAsFile(String resourcePath) {
	    try {
	        InputStream in = ReflectionUtils.class.getResourceAsStream(resourcePath);
	        if (in == null) {
	        	LoggerFactory.getLogger().error("getResourceAsFile(file) -> File not found! Returning null!");
	            return null;
	        }

	        File tempFile = File.createTempFile(String.valueOf(in.hashCode()), ".tmp");
	        tempFile.deleteOnExit();

	        final FileOutputStream out = new FileOutputStream(tempFile);
	        byte[] buffer = new byte[1024];
	        int bytesRead;
	        while ((bytesRead = in.read(buffer)) != -1) {
	            out.write(buffer, 0, bytesRead);
	        }
	        out.close();

	        return tempFile;
	    } catch (IOException e) {
	    	System.err.println("[FileUtil] Failed to read resource!");
	        e.printStackTrace();
	        return null;
	    }
	}
	
	public static <T> List<Class<?>> getTypeArguments(Class<T> baseClass,
			Class<? extends T> childClass) {
		Map<Type, Type> resolvedTypes = new HashMap<Type, Type>();
		Type type = childClass;
		// start walking up the inheritance hierarchy until we hit baseClass
		while (!getClass(type).equals(baseClass)) {
			if (type instanceof Class) {
				// there is no useful information for us in raw types, so just
				// keep going.
				type = ((Class) type).getGenericSuperclass();
			} else {
				ParameterizedType parameterizedType = (ParameterizedType) type;
				Class<?> rawType = (Class) parameterizedType.getRawType();

				Type[] actualTypeArguments = parameterizedType
						.getActualTypeArguments();
				TypeVariable<?>[] typeParameters = rawType.getTypeParameters();
				for (int i = 0; i < actualTypeArguments.length; i++) {
					resolvedTypes
							.put(typeParameters[i], actualTypeArguments[i]);
				}

				if (!rawType.equals(baseClass)) {
					type = rawType.getGenericSuperclass();
				}
			}
		}

		// finally, for each actual type argument provided to baseClass,
		// determine (if possible)
		// the raw class for that type argument.
		Type[] actualTypeArguments;
		if (type instanceof Class) {
			actualTypeArguments = ((Class) type).getTypeParameters();
		} else {
			actualTypeArguments = ((ParameterizedType) type)
					.getActualTypeArguments();
		}
		List<Class<?>> typeArgumentsAsClasses = new ArrayList<Class<?>>();
		// resolve types by chasing down type variables.
		for (Type baseType : actualTypeArguments) {
			while (resolvedTypes.containsKey(baseType)) {
				baseType = resolvedTypes.get(baseType);
			}
			typeArgumentsAsClasses.add(getClass(baseType));
		}
		return typeArgumentsAsClasses;
	}
	
	private static Class<?> getClass(Type type) {
		if (type instanceof Class) {
			return (Class) type;
		} else if (type instanceof ParameterizedType) {
			return getClass(((ParameterizedType) type).getRawType());
		} else if (type instanceof GenericArrayType) {
			Type componentType = ((GenericArrayType) type)
					.getGenericComponentType();
			Class<?> componentClass = getClass(componentType);
			if (componentClass != null) {
				return Array.newInstance(componentClass, 0).getClass();
			} else {
				return null;
			}
		} else {
			return null;
		}
	}
}
